Skip to content

Support tls certificates reload and crls #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 13, 2025
Merged

Conversation

kezhuw
Copy link
Owner

@kezhuw kezhuw commented Jul 7, 2025

This allows creating a client with dynamic tls certificates. When created this way, on reconnection the client will use latest tls certificates.

This allows us to reload refreshed certificates stored somewhere in client side.

This commit also adds support for crls in cert verifier to reject revoked server certs.

Resolves #59.

@kezhuw kezhuw force-pushed the tls-certs-reload branch 2 times, most recently from 2bc7594 to a7a17be Compare July 7, 2025 11:07
@kezhuw
Copy link
Owner Author

kezhuw commented Jul 7, 2025

@behos Would you mind take a look ? Does it solve your concerns ? I included a file-based certs in test.

struct FileBasedDynamicCerts {
ca: Ca,
dir: TempDir,
certs: TlsDynamicCerts,
feedback: UnboundedReceiver<()>,
_task: TaskHandle<()>,
}
struct EventSender {
sender: UnboundedSender<Event>,
}
impl notify::EventHandler for EventSender {
fn handle_event(&mut self, event: Result<Event, notify::Error>) {
if let Ok(event) = event {
self.sender.unbounded_send(event).unwrap();
}
}
}
impl FileBasedDynamicCerts {
pub fn new(ca: Ca, client_name: &str) -> Self {
let dir = TempDir::new().unwrap();
Self::generate_cert(&ca, dir.path(), client_name);
let (certs, feedback, _task) = Self::load_dynamic_certs(dir.path());
Self { ca, dir, certs, feedback, _task }
}
fn load_dynamic_certs(dir: &Path) -> (TlsDynamicCerts, UnboundedReceiver<()>, TaskHandle<()>) {
let cert_path = dir.join("cert.pem").to_path_buf();
let key_path = dir.join("cert.key.pem").to_path_buf();
let mut cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let mut key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();
let dynamic_certs =
TlsDynamicCerts::new(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
let dynamic_certs_updator = dynamic_certs.clone();
let (feedback_sender, feedback_receiver) = mpsc::unbounded();
let task = asyncs::spawn(async move {
let (tx, mut rx) = mpsc::unbounded();
let mut watcher = notify::recommended_watcher(EventSender { sender: tx }).unwrap();
watcher.watch(&cert_path, RecursiveMode::NonRecursive).unwrap();
watcher.watch(&key_path, RecursiveMode::NonRecursive).unwrap();
while rx.next().await.is_some() {
let updated_cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let updated_key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
if updated_cert_modified <= cert_modified || updated_key_modified <= key_modified {
continue;
}
cert_modified = updated_cert_modified;
key_modified = updated_key_modified;
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();
dynamic_certs_updator
.update(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
feedback_sender.unbounded_send(()).unwrap();
}
})
.attach();
(dynamic_certs, feedback_receiver, task)
}
fn generate_cert(ca: &Ca, dir: &Path, name: &str) {
let client_cert = generate_client_cert(name, &ca.issuer);
let file = AtomicWriteFile::open(dir.join("cert.pem")).unwrap();
write!(&file, "{}", client_cert.cert.pem()).unwrap();
file.commit().unwrap();
let file = AtomicWriteFile::open(dir.join("cert.key.pem")).unwrap();
write!(&file, "{}", client_cert.signing_key.serialize_pem()).unwrap();
file.commit().unwrap();
}
pub async fn regenerate_cert(&mut self, client_name: &str) {
Self::generate_cert(&self.ca, self.dir.path(), client_name);
self.feedback.next().await;
}
}
#[asyncs::test]
async fn with_file_based_dynamic_certs() {
let (ca, listener) = listen().await;
let options = TlsOptions::new().with_pem_ca_certs(&ca.cert.pem()).unwrap();
let mut certs = FileBasedDynamicCerts::new(ca, "client1");
let options = options.with_certs(certs.certs.clone());
let client = options.into_client().unwrap();
assert_client_name(&listener, &client, "client1").await;
certs.regenerate_cert("client2").await;
assert_client_name(&listener, &client, "client2").await;
}

@kezhuw kezhuw force-pushed the tls-certs-reload branch from a7a17be to a2cd525 Compare July 7, 2025 18:38
Copy link
Contributor

@behos behos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, the file based implementation can either be exposed here or delegated to external services.

Comment on lines 352 to 531
struct FileBasedDynamicCerts {
ca: Ca,
dir: TempDir,
certs: TlsDynamicCerts,
feedback: UnboundedReceiver<()>,
_task: TaskHandle<()>,
}

struct EventSender {
sender: UnboundedSender<Event>,
}

impl notify::EventHandler for EventSender {
fn handle_event(&mut self, event: Result<Event, notify::Error>) {
if let Ok(event) = event {
self.sender.unbounded_send(event).unwrap();
}
}
}

impl FileBasedDynamicCerts {
pub fn new(ca: Ca, client_name: &str) -> Self {
let dir = TempDir::new().unwrap();
Self::generate_cert(&ca, dir.path(), client_name);
let (certs, feedback, _task) = Self::load_dynamic_certs(dir.path());
Self { ca, dir, certs, feedback, _task }
}

fn load_dynamic_certs(dir: &Path) -> (TlsDynamicCerts, UnboundedReceiver<()>, TaskHandle<()>) {
let cert_path = dir.join("cert.pem").to_path_buf();
let key_path = dir.join("cert.key.pem").to_path_buf();

let mut cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let mut key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();

let dynamic_certs =
TlsDynamicCerts::new(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
let dynamic_certs_updator = dynamic_certs.clone();

let (feedback_sender, feedback_receiver) = mpsc::unbounded();
let task = asyncs::spawn(async move {
let (tx, mut rx) = mpsc::unbounded();
let mut watcher = notify::recommended_watcher(EventSender { sender: tx }).unwrap();
watcher.watch(&cert_path, RecursiveMode::NonRecursive).unwrap();
watcher.watch(&key_path, RecursiveMode::NonRecursive).unwrap();
while rx.next().await.is_some() {
let updated_cert_modified = std::fs::metadata(&cert_path).unwrap().modified().unwrap();
let updated_key_modified = std::fs::metadata(&key_path).unwrap().modified().unwrap();
if updated_cert_modified <= cert_modified || updated_key_modified <= key_modified {
continue;
}
cert_modified = updated_cert_modified;
key_modified = updated_key_modified;
let client_cert = std::fs::read_to_string(&cert_path).unwrap();
let client_key = std::fs::read_to_string(&key_path).unwrap();
dynamic_certs_updator
.update(TlsCerts::default().with_pem_identity(&client_cert, &client_key).unwrap());
feedback_sender.unbounded_send(()).unwrap();
}
})
.attach();
(dynamic_certs, feedback_receiver, task)
}

fn generate_cert(ca: &Ca, dir: &Path, name: &str) {
let client_cert = generate_client_cert(name, &ca.issuer);
let file = AtomicWriteFile::open(dir.join("cert.pem")).unwrap();
write!(&file, "{}", client_cert.cert.pem()).unwrap();
file.commit().unwrap();

let file = AtomicWriteFile::open(dir.join("cert.key.pem")).unwrap();
write!(&file, "{}", client_cert.signing_key.serialize_pem()).unwrap();
file.commit().unwrap();
}

pub async fn regenerate_cert(&mut self, client_name: &str) {
Self::generate_cert(&self.ca, self.dir.path(), client_name);
self.feedback.next().await;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's worth having a FileBased implementation into the main package so that it's easily reusable?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think such an official file based implementation may have biased on the change detection. And, worse, it will propgate this to applications. I think we should avoid such a thing in library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me! The example looks clean enough to use as a basis for an implementation anyway. Thanks

@behos behos mentioned this pull request Jul 8, 2025
Comment on lines 98 to 174
/// Cell to keep up to date [TlsCerts].
#[derive(Clone, Debug)]
pub struct TlsDynamicCerts {
certs: Arc<RwLock<(u64, Arc<TlsCerts>)>>,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to use this from the branch and it seems it's not visible.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My fault! I have referenced it in integration test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I was able to create and test a simple file-based reloader that can be plugged into the new structs. It works great

@kezhuw kezhuw force-pushed the tls-certs-reload branch 8 times, most recently from e3adbc4 to c70064a Compare July 13, 2025 08:45
This allows creating a client with dynamic tls certificates. When
created this way, on reconnection the client will use latest tls
certificates.

This allows us to reload refreshed certificates stored somewhere in
client side.

This commit also adds support for crls in cert verifier to reject
revoked server certs.

Resolves #59.
@kezhuw kezhuw force-pushed the tls-certs-reload branch from c70064a to b141738 Compare July 13, 2025 08:47
@kezhuw kezhuw changed the title Support client side tls certificates reload Support tls certificates reload and crls Jul 13, 2025
@kezhuw kezhuw enabled auto-merge (squash) July 13, 2025 08:51
@kezhuw kezhuw merged commit c2ce0d8 into master Jul 13, 2025
12 checks passed
@kezhuw kezhuw deleted the tls-certs-reload branch July 13, 2025 08:58
Copy link

codecov bot commented Jul 13, 2025

Codecov Report

Attention: Patch coverage is 86.76976% with 77 lines in your changes missing coverage. Please review.

Project coverage is 85.83%. Comparing base (16e412f) to head (b141738).
Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/tls/options.rs 82.23% 27 Missing ⚠️
src/tls/verifier.rs 67.07% 27 Missing ⚠️
src/tls/client.rs 92.92% 23 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #66      +/-   ##
==========================================
+ Coverage   85.53%   85.83%   +0.30%     
==========================================
  Files          37       39       +2     
  Lines        5323     5782     +459     
==========================================
+ Hits         4553     4963     +410     
- Misses        770      819      +49     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants